﻿#region Using Statements
using System;
using System.Collections.Generic;
using GameBase.Graphics;
using GameBase.Screens;
using GameBase.Sprites;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
#endregion

namespace StarFox.ScreenComponents
{
    /// <summary>
    /// Implementation of a lens-flare effect.
    /// </summary>
    public class LensFlare : ScreenComponent3D, IDisposable
    {
        #region Types

        /// <summary>
        /// Stores data of each individual graphic associated to a lens-flare effect.
        /// </summary>
        public class Flare
        {
            #region Properties

            /// <summary>
            /// Gets the relative position of the graphic, in a line between the light source (0.0), and the center of the screen (1.0).
            /// </summary>
            public float Position { get; private set; }

            /// <summary>
            /// Gets the scale factor of the graphic. 
            /// </summary>
            public float Scale { get; private set; }

            /// <summary>
            /// Gets the color to tint the graphic.
            /// </summary>
            public Color Color { get; private set; }

            /// <summary>
            /// Gets the asset name of the texture associated to the graphic.
            /// </summary>
            public string Texture { get; private set; }

            #endregion

            #region Constructor

            /// <summary>
            /// Initializes a new instance of this class.
            /// </summary>
            /// <param name="position">The relative position of the graphic.</param>
            /// <param name="scale">The scale factor of the graphic.</param>
            /// <param name="color">The color to tint the graphic.</param>
            /// <param name="texture">The asset name of the texture associated to the graphic.</param>
            public Flare(float position, float scale, Color color, string texture)
            {
                Position = position;
                Scale = scale;
                Color = color;
                Texture = texture;
            }

            #endregion
        }

        #endregion

        #region Fields

        SpriteSheet spriteSheet;

        readonly List<Flare> flareList;
        readonly float querySize;
        
        OcclusionQuery query;
        bool isQueryActive;
        VertexPosition[] queryQuad;
        BasicEffect basicEffect;
        SpriteBatch SpriteBatch;
        BlendState BlendAdditive;

        bool isLightVisible;
        Vector2 lightPosition;

        // 0 -> totally occluded; 1 -> totally visible;
        float occlusion;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the direction of the light source associated to the lens-flare effect.
        /// </summary>
        public Vector3 LightDirection { get; set; }

        /// <summary>
        /// Gets or sets the color of the light source associated to the lens-flare effect.
        /// </summary>
        public Color LightColor { get; set; }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of this class.
        /// </summary>
        /// <param name="screen">The Screen that this ScreenComponent3D is associated to.</param>
        /// <param name="flareList">List of individual graphics associated to the lens-flare effect.</param>
        /// <param name="querySize">The size of the quead used for occlusion testing in a lens-flare effect.</param>
        public LensFlare(Screen screen, List<Flare> flareList, float querySize)
            : base(screen)
        {
            this.flareList = flareList;
            this.querySize = querySize;

            spriteSheet = Screen.Content.Load<SpriteSheet>(ContentSettings.GetAsset("LENSFLARE_SPRITES"));

            LightDirection = Vector3.Backward;
            LightColor = Color.White;

            SpriteBatch = Screen.SpriteBatch;
            BlendAdditive = new BlendState()
            {
                ColorSourceBlend = Blend.SourceAlpha,
                AlphaSourceBlend = Blend.SourceAlpha,
                ColorDestinationBlend = Blend.One,
                AlphaDestinationBlend = Blend.One,
            };

            basicEffect = new BasicEffect(GraphicsDevice);
            query = new OcclusionQuery(GraphicsDevice);

            queryQuad = new VertexPosition[4];
            queryQuad[0] = new VertexPosition(new Vector3(-querySize / 2, -querySize / 2, -1));
            queryQuad[1] = new VertexPosition(new Vector3(querySize / 2, -querySize / 2, -1));
            queryQuad[2] = new VertexPosition(new Vector3(-querySize / 2, querySize / 2, -1));
            queryQuad[3] = new VertexPosition(new Vector3(querySize / 2, querySize / 2, -1));
        }

        #endregion

        #region Dispose

        // dispose flag
        bool isDisposed = false;

        /// <summary>
        /// Immediately releases all resources used by this object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Allows an object to try to free resources and perform other cleanup operations before it is reclaimed by garbage collection.
        /// </summary>
        ~LensFlare()
        {
            Dispose(false);
        }

        /// <summary>
        /// Releases the unmanaged resources used by this object and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!isDisposed)
            {
                if (disposing)
                {
                    if (BlendAdditive != null) { BlendAdditive.Dispose(); BlendAdditive = null; }
                    if (basicEffect != null) { basicEffect.Dispose(); basicEffect = null; }
                    if (query != null) { query.Dispose(); query = null; }
                }

                isDisposed = true;
            }
        }

        #endregion

        #region Update

        /// <summary>
        /// Called when the ScreenComponent3D needs to be updated.
        /// </summary>
        /// <param name="gameTime">Time elapsed since the last call to this method.</param>
        public override void Update(GameTime gameTime)
        {
        }

        #endregion

        #region Draw

        /// <summary>
        /// Called when the ScreenComponent3D needs to be drawn.
        /// </summary>
        /// <param name="gameTime">Time elapsed since the last call to this method.</param>
        public override void Draw(GameTime gameTime) 
        {
            OcclusionQuery();

            // ... light source not in direct visibility
            // ... light source totally occluded
            if (!isLightVisible || occlusion == 0.0f)
                return;

            DrawLensFlare();

            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
        }

        #region OcclusionQuery

        /// <summary>
        /// Performs an occlusion query and determines how much occluded the light source is.
        /// </summary>
        private void OcclusionQuery()
        {
            // adjust view matrix
            Matrix auxView = Matrix.Invert(Camera.View);
            auxView.Translation = Vector3.Zero;
            auxView = Matrix.Invert(auxView);

            // project the ligth source into screen space
            Vector3 scrProj = GraphicsDevice.Viewport.Project(-LightDirection, Camera.Projection, auxView, Matrix.Identity);

            // determine whether the light source is in direct visibility
            if ((scrProj.Z < 0) || (scrProj.Z > 1))
            {
                isLightVisible = false;
                return;
            }
            else
            {
                isLightVisible = true;
                lightPosition = new Vector2(scrProj.X, scrProj.Y);
            }

            if (isQueryActive)
            {
                // wait until the previous query is complete (in a previous frame)
                if (!query.IsComplete)
                    return;

                occlusion = MathHelper.Clamp(query.PixelCount / (querySize * querySize), 0.0f, 1.0f);
            }

            // ... start a new query

            // before drawing the occlusion query quad, disable any color write and depth write
            GraphicsDevice.BlendState = new BlendState() { ColorWriteChannels = ColorWriteChannels.None };
            GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
            GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;

            // set the BasicEffect
            basicEffect.World = Matrix.CreateTranslation(lightPosition.X, lightPosition.Y, 0);
            basicEffect.Projection = Matrix.CreateOrthographicOffCenter(0, Screen.Width, Screen.Height, 0, 0, 1);
            basicEffect.CurrentTechnique.Passes[0].Apply();

            query.Begin();
            GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, queryQuad, 0, 2);
            query.End();

            isQueryActive = true;
        }

        #endregion

        #region DrawLensFlare

        /// <summary>
        /// Draws all the graphics included in the lens-flare effect.
        /// </summary>
        private void DrawLensFlare()
        {
            SpriteBatch.Begin(0, BlendAdditive);

            Vector2 screenCenter = new Vector2(Screen.Width, Screen.Height) / 2;
            Vector2 flareDirection = screenCenter - lightPosition;

            foreach (Flare flare in flareList)
            {
                Vector2 flarePosition = lightPosition + flareDirection * flare.Position;
                Vector2 flareOrigin = new Vector2((float)spriteSheet.SourceRectangle(flare.Texture).Width / 2,
                                                  (float)spriteSheet.SourceRectangle(flare.Texture).Height / 2);

                Color flareColor = new Color(flare.Color.ToVector4() * LightColor.ToVector4()) * occlusion;
                float flareScale = flare.Scale * Screen.Scale;

                SpriteBatch.Draw(spriteSheet.Texture, flarePosition, spriteSheet.SourceRectangle(flare.Texture),
                                 flareColor, 0.0f, flareOrigin, flareScale, SpriteEffects.None, 0.0f);
            }

            SpriteBatch.End();
        }

        #endregion

        #endregion
    }
}
